package org.ezaero.jmimic.api;

import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.Map;

import org.ezaero.jmimic.impl.proxy.MimicHandler;
import org.ezaero.jmimic.impl.store.MimicStore;
import org.ezaero.jmimic.impl.store.xstream.XStreamMimicStore;

/**
 * Provides the means to create and destroy a proxy that mimics calls onto or calls from some external system.
 * <p>
 * The mimicry is recorded if no mimic file exists, and is replayed if a mimic file does exist. A mimic file can be
 * regenerating by deleting the mimic file and re-running your mimicry.
 * <p>
 * Typical usage for mimicing method calls looks something like:
 * <code>
 *  MimicConfig config = new MimicConfig();
 *  //set your configuration
 *  Mimicry mimcry = new Mimicry(config);
 *  MyInterface proxy = mimicry.mimicFrom("MyMimicInstanceName", MyInterface.class, myMimicedInstance);
 *  try {
 *      //use the proxy
 *  } finally {
 *      mimicry.close(proxy);
 *  }
 *  </code>
 * 
 * @see MimicConfig
 * @author alex.radeski
 */
public class Mimicry {

    private final MimicConfig config;

    private final Map<Object, MimicStore> proxyStores = new HashMap<Object, MimicStore>();

    /**
     * Construct an instance with the specified configuration.
     */
    public Mimicry(MimicConfig config) {
        this.config = config;
    }

    /**
     * This method is used to mimic method calls from your application onto a source system. A proxy is created from
     * the specified parameters and is used to either record or replay a mimicry session.
     * <p>
     * This is the most common form of mimicry, and should be used when you want to mimic calls onto an interface to an
     * external system.
     * 
     * @param <T> the return type.
     * @param name the name of this single mimicry instance. This will form part of the file path used to store mimicry
     *            data.
     * @param iface the interface used to create a mimic proxy.
     * @param source an instance that will act as the source implemetation of the interface. When recording a mimicry,
     *            all invocations on the proxy will be delegated to this instance.
     * @return the pull mimic proxy.
     */
    public <T> T mimicPull(String name, Class<T> iface, T source) {
        final MimicStore store = new XStreamMimicStore(config, name);
        return createProxy(iface, source, store, false);
    }

    /**
     * This method is used to mimic method calls from a source system onto your application. A proxy is created from
     * the specified parameters and is used to either record or replay a mimicry session.
     * <p>
     * To create a push mimic you register the proxy as a callback with the source system, and it will intercept method invocations
     * received and then pass them on to the target system. In this case the target is your application.
     * 
     * @param <T> the return type.
     * @param name the name of this single mimicry instance. This will form part of the file path used to store mimicry
     *            data.
     * @param iface the interface used to create a mimic proxy. This will usually be some form of callback or listener.
     * @param target 
     * @return the push mimic proxy.
     */
    public <T> T mimicPush(String name, Class<T> iface, T target) {
        final MimicStore store = new XStreamMimicStore(config, name);
        return createProxy(iface, target, store, true);
    }

    /**
     * Close a mimicry session for the specified proxy. This is required to produce valid mimicry data.
     * 
     * @param <T>
     * @param proxy
     */
    public <T> void close(T proxy) {
        final MimicStore mimicStore = proxyStores.get(proxy);
        if (mimicStore != null) {
            mimicStore.close();
        }
    }

    @SuppressWarnings("unchecked")
    protected <T> T createProxy(Class<T> iface, T target, final MimicStore store, boolean pushMode) {
        final Object object = Proxy.newProxyInstance(iface.getClassLoader(), new Class[] { iface }, new MimicHandler<T>(config, store, target, pushMode));

        proxyStores.put(object, store);

        return (T) object;
    }

}
